package com.zoe.onelink.core.web.resolver;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.zoe.onelink.annotation.web.arg.RequestArg;
import com.zoe.onelink.common.enumeration.ExceptionCodeEnum;
import com.zoe.onelink.common.exception.ApiException;
import com.zoe.onelink.core.property.OnelinkProperties;
import com.zoe.onelink.util.SpringUtil;
import com.zoe.onelink.util.StrUtil;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * <p>标题: 请求入参分解器抽象类</p>
 * <p>描述: </p>
 * <p>版权: Copyright (c) 2021</p>
 * <p>公司: 智业软件股份有限公司</p>
 *
 * @version: 1.0
 * @author: ruansheng
 * @date: 2021-11-04
 */
@Slf4j
public abstract class AbstractObjectMapperMethodArgumentResolver implements HandlerMethodArgumentResolver {

    protected static final String CACHED_JSON_NODE = "CACHED_JSON_NODE";
    protected static final String PAGE_NODE_NAME = "page";
    protected static final String PAGE_SIZE_NODE_NAME = "pageSize";
    protected static final String UNDERLINE = "_";
    protected static final String EMPTY_JSON_OBJ = "{}";

    protected final ObjectMapper om;
    protected final OnelinkProperties.Web.Arg requestArgProp;

    public AbstractObjectMapperMethodArgumentResolver(ObjectMapper objectMapper, OnelinkProperties.Web.Arg requestArgProp) {
        this.om = objectMapper;
        this.requestArgProp = requestArgProp;
        Assert.notNull(this.om);
        Assert.notNull(this.requestArgProp);
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return this.isApplicationJsonRequest() && this.couldResolveArgument(parameter);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        this.logDebug(webRequest);
        try {
            JsonNode rootNode = this.getRootNode(webRequest);
            Type type = GenericTypeResolver.resolveType(ResolvableType.forMethodParameter(parameter).getType(), parameter.getContainingClass());
            JavaType javaType = this.om.getTypeFactory().constructType(type);
            if (rootNode == null || rootNode.isEmpty()) {
                return this.isGenericType(javaType.getRawClass()) ? null : this.om.readValue(EMPTY_JSON_OBJ, javaType);
            }
            RequestArg requestArg = parameter.getParameterAnnotation(RequestArg.class);
            String paramName;
            if (requestArg == null) {
                paramName = parameter.getParameterName();
            } else {
                paramName = StrUtil.isBlank(requestArg.fieldName()) ? parameter.getParameterName() : requestArg.fieldName();
            }
            return this.resolveArgument(rootNode, javaType, requestArg, paramName, parameter.getParameterIndex());
        } catch (ApiException e) {
            throw e;
        } catch (Exception e) {
            throw ApiException.createEx(e, ExceptionCodeEnum.REQ_ARG_JSON_PARSE_EXCEPTION, parameter.getParameterIndex(), parameter.getParameterName());
        }
    }

    private void logDebug(NativeWebRequest webRequest) {
        if (log.isDebugEnabled()) {
            if (webRequest instanceof ServletWebRequest) {
                HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
                log.debug("使用 @RequestArg 参数解析器 , [{}] [{}] [{}]",
                        request.getMethod(), request.getRequestURI(), request.getContentType());
            }
        }
    }

    protected abstract boolean couldResolveArgument(MethodParameter parameter);

    protected abstract Object resolveArgument(JsonNode rootNode, JavaType paramType, RequestArg requestArg, String paramName, int paramIndex) throws Exception;

    /**
     * 判断是否是application/json请求
     */
    protected boolean isApplicationJsonRequest() {
        HttpServletRequest request = SpringUtil.getRequest();
        if (request == null) {
            return false;
        }
        String contentType = request.getContentType();
        if (contentType == null) {
            return false;
        }
        return contentType.toLowerCase().startsWith(MediaType.APPLICATION_JSON_VALUE);
    }

    /**
     * 获取请求体JSON字符串
     */
    protected JsonNode getRootNode(NativeWebRequest webRequest) throws IOException {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        JsonNode rootNode = (JsonNode) webRequest.getAttribute(CACHED_JSON_NODE, NativeWebRequest.SCOPE_REQUEST);
        if (rootNode == null && servletRequest != null) {
            // 双重检查: Spring 针对resolver有缓存机制,第一次请求为application/json会缓存该resolver,下次用其他content-type请求
            // 会根据parameter查找缓存的resolver,所以如果第一次该resolver被命中,那么下次其他content-type也会进入该resolver
            String contentType = servletRequest.getContentType();
            String reqBody = SpringUtil.getRequestBody(servletRequest);
            // ContentType不是application/json时的处理逻辑
            if (!StrUtil.containsIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE)) {
                log.warn("意料之外的Content-Type: {} , Uri: {}", contentType, servletRequest.getRequestURI());
                if (StrUtil.isBlank(reqBody)) {
                    Map<String, String[]> parameterMap = servletRequest.getParameterMap();
                    JSONObject paramJson = new JSONObject();
                    parameterMap.forEach((k, v) -> {
                        String val;
                        if (v == null || v.length == 0) {
                            val = null;
                        } else if (v.length == 1) {
                            val = ObjectUtil.defaultIfNull(v[0], "");
                        } else {
                            val = JSON.toJSONString(v);
                        }
                        paramJson.put(k, val);
                    });
                    reqBody = paramJson.toJSONString();
                } else {
                    JSONObject paramJson = new JSONObject();
                    String[] params = reqBody.split("&");
                    for (String param : params) {
                        String[] paramArr = param.split("=");
                        paramJson.put(paramArr[0], paramArr[1]);
                    }
                    reqBody = paramJson.toJSONString();
                }
            }
            rootNode = reqBody == null ? null : this.om.readTree(reqBody);
            // 扁平化参数
            if (rootNode != null && this.requestArgProp.isFlatten()) {
                // 请求入参是数组格式
                if (rootNode instanceof ArrayNode) {
                    ObjectNode objNode = this.om.createObjectNode();
                    objNode.set("root", rootNode);
                    this.flattenJsonNode(objNode);
                    // 请求入参是对象格式
                } else {
                    this.flattenJsonNode((ObjectNode) rootNode);
                }
            }
            servletRequest.setAttribute(CACHED_JSON_NODE, rootNode);
        }
        return rootNode;
    }


    /**
     * 扁平化JSON节点
     *
     * @param rootNode 根节点
     */
    protected void flattenJsonNode(ObjectNode rootNode) throws JsonProcessingException {

        ObjectNode bufferNode = (ObjectNode) this.om.readTree(EMPTY_JSON_OBJ);

        List<String> rootNodeFieldNames;
        if (log.isWarnEnabled()) {
            rootNodeFieldNames = IterUtil.toList(rootNode.fieldNames());
            rootNodeFieldNames.removeIf(PAGE_NODE_NAME::equals);
        } else {
            rootNodeFieldNames = Collections.emptyList();
        }
        Iterator<Map.Entry<String, JsonNode>> fields = rootNode.fields();
        while (fields.hasNext()) {
            Map.Entry<String, JsonNode> nodeEntry = fields.next();
            String nodeName = nodeEntry.getKey();
            // 分页节点不做扁平化
            if (PAGE_NODE_NAME.equals(nodeName)) {
                continue;
            }
            JsonNode jsonNode = nodeEntry.getValue();
            // 如果不是对象节点,直接跳过
            if (!jsonNode.isObject()) {
                continue;
            }

            // 重复字段名警告
            if (log.isWarnEnabled()) {
                List<String> subNodeFieldNames = IterUtil.toList(jsonNode.fieldNames());
                Collection<String> repeatFieldName = CollUtil.intersection(rootNodeFieldNames, subNodeFieldNames);
                if (CollUtil.isNotEmpty(repeatFieldName)) {
                    log.warn("[请求参数扁平化] 请求参数存在重复的字段名: " + repeatFieldName);
                }
            }

            // 扁平化策略=覆盖
            OnelinkProperties.Web.Arg.FlattenOverwriteStrategy flattenOverwriteStrategy = this.requestArgProp.getFlattenOverwriteStrategy();
            if (flattenOverwriteStrategy == OnelinkProperties.Web.Arg.FlattenOverwriteStrategy.ALWAYS_OVERWRITE) {
                bufferNode.setAll((ObjectNode) jsonNode);

                continue;
            }

            Iterator<String> fieldNameIt = jsonNode.fieldNames();
            while (fieldNameIt.hasNext()) {
                String fieldName = fieldNameIt.next();
                JsonNode subNode = jsonNode.get(fieldName);
                // 字段不存在时,直接加入
                if (!rootNode.has(fieldName)) {
                    bufferNode.set(fieldName, subNode);
                    continue;
                }

                // ===== 以下是字段重复的情况 =====
                // 扁平化策略=在字段前面加下划线
                if (flattenOverwriteStrategy == OnelinkProperties.Web.Arg.FlattenOverwriteStrategy.UNDERLINE_IF_KEY_REPEAT) {
                    bufferNode.set(UNDERLINE.concat(fieldName), subNode);
                    // 扁平化策略=字段重复时直接抛出异常
                } else if (flattenOverwriteStrategy == OnelinkProperties.Web.Arg.FlattenOverwriteStrategy.THROW_IF_KEY_REPEAT) {
                    throw ApiException.createEx(ExceptionCodeEnum.REQ_ARG_FLATTEN_JSON_KEY_REPEAT, fieldName);
                }
                // 走到这里说明策略是不覆盖,直接跳过
            }
        }
        rootNode.setAll(bufferNode);
    }

    /**
     * 是否是普通类型 (普通的字符串、数字、日期等类型)
     */
    protected boolean isGenericType(@NonNull Class<?> clazz) {
        if (clazz.isPrimitive()) {
            return true;
        }
        return BeanUtils.isSimpleValueType(clazz);
    }

}
